package org.bhf.service;

/**
 * Copyright (c) 2007, Blue Hole Software. All rights reserved.
 * Code licensed under the Apache 2.0 License:
 * http://www.apache.org/licenses/
 */

import com.google.inject.Injector;
import org.bhf.i18n.Message;
import org.bhf.i18n.MessageType;

import java.io.Serializable;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.logging.Level;

/**
 * <p>Represents a group of related Services.</p>
 *
 * <p><code>ServiceGroup</code>s are shared across requests and must be thread safe</p>
 */
public class ServiceGroup implements Serializable
{
    private static final long serialVersionUID = -5107736412242358354L;

    private static final Map<Class<?>, ServiceGroup>    serviceGroups        = Collections.synchronizedMap( new HashMap<Class<?>, ServiceGroup>() );
    private static final Map<String, ServiceGroup>      namedServiceGroups   = Collections.synchronizedMap( new HashMap<String, ServiceGroup>() );

    private         String                  viewBase;
    private         String                  localizationBase;
    private         ServiceGroup            parentServiceGroup;
    private final   Map<String,Class<?>>    serviceClasses = new HashMap<String,Class<?>>();

    //
    // Mapping of Service class to ServiceGroup class
    //

    /**
     * Return the ServiceGroup that hosts the given Service.
     * @param service Class of Service
     * @return The ServiceGroup that hosts the given Service.
     */
    public static ServiceGroup getServiceGroupForService( final Class<?> service )
    {
        return serviceGroups.get( service );
    }

    /**
     * Return the ServiceGroup with the given name.
     * @param serviceGroupName ServiceGroup name, as specified in
     * @return The ServiceGroup with the given name.
     */
    public static ServiceGroup getServiceGroupByName( final String serviceGroupName )
    {
        return namedServiceGroups.get( serviceGroupName );
    }

    /**
     * Construct a <code>ServiceGroup</code>, using convenions for the localization base and view base.
     *
     * @param parentServiceGroup The parent <code>ServiceGroup</code>, or <code>null</code> if none.
     */
    public ServiceGroup( final ServiceGroup parentServiceGroup )
    {
        this( parentServiceGroup, null, null );
    }

    /**
     * Construct a <code>ServiceGroup</code>.
     *
     * @param localizationBase Location of the locaization resource bundle, relative to the CLASSPATH
     * @param viewBase  View base as a contract between the configuration and the view resolver.
     * @param parentServiceGroup The parent <code>ServiceGroup</code>, or <code>null</code> if none.
     */
    public ServiceGroup( final ServiceGroup parentServiceGroup, final String localizationBase, final String viewBase )
    {
        // Simple properties
        this.viewBase           = viewBase != null ? viewBase : "/WEB-INF/" + getName() + "/views/";
        this.parentServiceGroup = parentServiceGroup;
        this.localizationBase   = localizationBase != null ? localizationBase : "/" + getName() + "/";

        // Load all of our Services and register any public services
        {
            final Class<?>[] services = ServiceGroupInfo.Util.services( this.getClass() );
            if( services != null && services.length > 0 )
            {
                for( Class<?> service : services )
                {
                    final Service serviceName = Service.Util.get( service );

                    if( serviceClasses.put( serviceName.name(), service ) != null )
                        Package.log.log( Level.WARNING, "Duplicate serviceName name (" + serviceName.name() + ") for serviceGroup " + getName() );
                    serviceGroups.put( service, this );
                }
            }
        }

        //
        namedServiceGroups.put( getName(), this );
    }

    //
    // Message Factory
    //

    /**
     * Create an error message that uses the 'errors' resource bundle of this serviceGroup.
     *
     * @param topic If this message is associated with a property on the service, the property
     *  path, such as <code>foo.bar[3].name</code>. If this message is not associated with any
     *  property, the topic may be any other appropriate string. Must not be <code>null</code>.
     * @param summary Must not be <code>null</code>.
     * @param detail If <code>null</code>, defaults to the summary.
     * @param parameters Message substitution parameters.
     *
     * @return A <code>Message</code>
     */
    public Message newError(
        final String            topic,
        final String            summary,
        final String            detail,
        final Object...         parameters
    )
    {
        return MessageType.newError( topic, summary, detail, getLocalizationBase() + "errors", parameters );
    }

    /**
     * Create an informational message that uses the 'messages' resource bundle of this serviceGroup.
     *
     * @param topic If this message is associated with a property on the service, the property
     *  path, such as <code>foo.bar[3].name</code>. If this message is not associated with any
     *  property, the topic may be any other appropriate string. Must not be <code>null</code>.
     * @param summary Must not be <code>null</code>.
     * @param detail If <code>null</code>, defaults to the summary.
     * @param parameters Message substitution parameters.
     *
     * @return A <code>Message</code>
     */
    public Message newInfo(
        final String            topic,
        final String            summary,
        final String            detail,
        final Object...         parameters
    )
    {
        return MessageType.newInfo( topic, summary, detail, getLocalizationBase() + "messages", parameters );
    }

    //
    // Properties and factories
    //

    /**
     * Return the view base for accessing the views of this <code>ServiceGroup</code>.
     * @return the view base for accessing the views of this <code>ServiceGroup</code>.
     */
    public String               getViewBase()
    {
        return viewBase;
    }

    /**
     * Return the localization resource bundle, relative to the CLASSPATH.
     * @return the localization resource bundle, relative to the CLASSPATH.
     */
    public String               getLocalizationBase()
    {
        return localizationBase;
    }

    /**
     * Return the parent of this <code>ServiceGroup</code>, if any.
     * @return The parent of this <code>ServiceGroup</code>, if any. <code>null</code> if none.
     */
    public ServiceGroup         getParentServiceGroup()
    {
        return parentServiceGroup;
    }

    /**
     * Return the name of this serviceGroup.
     * @return The name of this serviceGroup.
     */
    public String               getName()
    {
        return ServiceGroupInfo.Util.getName( getClass() );
    }

    /**
     * Create a new <code>ServiceName</code>.
     *
     * @param serviceName Factory name of <code>ServiceName</code>.
     * @param injector Injector to use when instantiating the service
     *
     * @return The new <code>ServiceName</code>, or <code>null</code> if the name is unknown.
     */
    public synchronized Object newService( final String serviceName, final Injector injector )
    {
        final Class<?>  serviceClass = serviceClasses.get( serviceName );
        final Object    service;

        if( serviceClass != null )
        {
            try
            {
                service = injector.getInstance( serviceClass );
                serviceGroups.put( service.getClass(), this ); // Guice may subclass, so we have to register the subclass as well
            }
            catch( final Exception e )
            {
                throw new RuntimeException( e ); // TODO Do we really want to soften
            }
        }
        else
        {
            service = null;
        }

        return service;
    }
}
